Passed
Pull Request — main (#339)
by Alejandro
04:00
created

ShlinkApiClient.getShortUrl   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 3
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
crap 1
1
import qs from 'qs';
2
import { isEmpty, isNil, reject } from 'ramda';
3
import { AxiosInstance, AxiosResponse, Method } from 'axios';
4
import { ShortUrlsListParams } from '../../short-urls/reducers/shortUrlsListParams';
5
import { ShortUrl, ShortUrlData } from '../../short-urls/data';
6
import { OptionalString } from '../utils';
7
import {
8
  ShlinkHealth,
9
  ShlinkMercureInfo,
10
  ShlinkShortUrlsResponse,
11
  ShlinkTags,
12
  ShlinkTagsResponse,
13
  ShlinkVisits,
14
  ShlinkVisitsParams,
15
  ShlinkShortUrlMeta,
16
  ShlinkDomain,
17
  ShlinkDomainsResponse,
18
} from './types';
19
20 23
const buildShlinkBaseUrl = (url: string, apiVersion: number) => url ? `${url}/rest/v${apiVersion}` : '';
21 2
const rejectNilProps = reject(isNil);
22
23
export default class ShlinkApiClient {
24
  private apiVersion: number;
25
26
  public constructor(
27
    private readonly axios: AxiosInstance,
28
    private readonly baseUrl: string,
29
    private readonly apiKey: string,
30
  ) {
31 27
    this.apiVersion = 2;
32
  }
33
34 27
  public readonly listShortUrls = async (params: ShortUrlsListParams = {}): Promise<ShlinkShortUrlsResponse> =>
35 1
    this.performRequest<{ shortUrls: ShlinkShortUrlsResponse }>('/short-urls', 'GET', params)
36 1
      .then(({ data }) => data.shortUrls);
37
38 27
  public readonly createShortUrl = async (options: ShortUrlData): Promise<ShortUrl> => {
39 4
    const filteredOptions = reject((value) => isEmpty(value) || isNil(value), options as any);
40
41 2
    return this.performRequest<ShortUrl>('/short-urls', 'POST', {}, filteredOptions)
42 2
      .then((resp) => resp.data);
43
  };
44
45 27
  public readonly getShortUrlVisits = async (shortCode: string, query?: ShlinkVisitsParams): Promise<ShlinkVisits> =>
46 1
    this.performRequest<{ visits: ShlinkVisits }>(`/short-urls/${shortCode}/visits`, 'GET', query)
47 1
      .then(({ data }) => data.visits);
48
49 27
  public readonly getTagVisits = async (tag: string, query?: Omit<ShlinkVisitsParams, 'domain'>): Promise<ShlinkVisits> =>
50 1
    this.performRequest<{ visits: ShlinkVisits }>(`/tags/${tag}/visits`, 'GET', query)
51 1
      .then(({ data }) => data.visits);
52
53 27
  public readonly getShortUrl = async (shortCode: string, domain?: OptionalString): Promise<ShortUrl> =>
54 3
    this.performRequest<ShortUrl>(`/short-urls/${shortCode}`, 'GET', { domain })
55 3
      .then(({ data }) => data);
56
57 27
  public readonly deleteShortUrl = async (shortCode: string, domain?: OptionalString): Promise<void> =>
58 3
    this.performRequest(`/short-urls/${shortCode}`, 'DELETE', { domain })
59
      .then(() => {});
60
61 27
  public readonly updateShortUrlTags = async (
62
    shortCode: string,
63
    domain: OptionalString,
64
    tags: string[],
65
  ): Promise<string[]> =>
66 3
    this.performRequest<{ tags: string[] }>(`/short-urls/${shortCode}/tags`, 'PUT', { domain }, { tags })
67 3
      .then(({ data }) => data.tags);
68
69 27
  public readonly updateShortUrlMeta = async (
70
    shortCode: string,
71
    domain: OptionalString,
72
    meta: ShlinkShortUrlMeta,
73
  ): Promise<ShlinkShortUrlMeta> =>
74 3
    this.performRequest(`/short-urls/${shortCode}`, 'PATCH', { domain }, meta)
75 3
      .then(() => meta);
76
77 27
  public readonly listTags = async (): Promise<ShlinkTags> =>
78 1
    this.performRequest<{ tags: ShlinkTagsResponse }>('/tags', 'GET', { withStats: 'true' })
79 1
      .then((resp) => resp.data.tags)
80 1
      .then(({ data, stats }) => ({ tags: data, stats }));
81
82 27
  public readonly deleteTags = async (tags: string[]): Promise<{ tags: string[] }> =>
83 1
    this.performRequest('/tags', 'DELETE', { tags })
84 1
      .then(() => ({ tags }));
85
86 27
  public readonly editTag = async (oldName: string, newName: string): Promise<{ oldName: string; newName: string }> =>
87 1
    this.performRequest('/tags', 'PUT', {}, { oldName, newName })
88 1
      .then(() => ({ oldName, newName }));
89
90 27
  public readonly health = async (): Promise<ShlinkHealth> =>
91 1
    this.performRequest<ShlinkHealth>('/health', 'GET')
92 1
      .then((resp) => resp.data);
93
94 27
  public readonly mercureInfo = async (): Promise<ShlinkMercureInfo> =>
95 1
    this.performRequest<ShlinkMercureInfo>('/mercure-info', 'GET')
96 1
      .then((resp) => resp.data);
97
98 27
  public readonly listDomains = async (): Promise<ShlinkDomain[]> =>
99 1
    this.performRequest<{ domains: ShlinkDomainsResponse }>('/domains', 'GET').then(({ data }) => data.domains.data);
100
101 27
  private readonly performRequest = async <T>(url: string, method: Method = 'GET', query = {}, body = {}): Promise<AxiosResponse<T>> => {
102 23
    try {
103 23
      return await this.axios({
104
        method,
105
        url: `${buildShlinkBaseUrl(this.baseUrl, this.apiVersion)}${url}`,
106
        headers: { 'X-Api-Key': this.apiKey },
107
        params: rejectNilProps(query),
108
        data: body,
109
        paramsSerializer: (params) => qs.stringify(params, { arrayFormat: 'brackets' }),
110
      });
111
    } catch (e) {
112
      const { response } = e;
113
114
      // Due to a bug on all previous Shlink versions, requests to non-matching URLs will always result on a CORS error
115
      // when performed from the browser (due to the preflight request not returning a 2xx status.
116
      // See https://github.com/shlinkio/shlink/issues/614), which will make the "response" prop not to be set here.
117
      // The bug will be fixed on upcoming Shlink patches, but for other versions, we can consider this situation as
118
      // if a request has been performed to a not supported API version.
119
      const apiVersionIsNotSupported = !response;
120
121
      // When the request is not invalid or we have already tried both API versions, throw the error and let the
122
      // caller handle it
123 4
      if (!apiVersionIsNotSupported || this.apiVersion === 1) {
124
        throw e;
125
      }
126
127
      this.apiVersion = this.apiVersion - 1;
128
129
      return await this.performRequest(url, method, query, body);
130
    }
131
  };
132
}
133